Android 10、11 存储完全适配!(建议收藏)

您所在的位置:网站首页 android 11 权限适配 Android 10、11 存储完全适配!(建议收藏)

Android 10、11 存储完全适配!(建议收藏)

2023-04-11 17:58| 来源: 网络整理| 查看: 265

就可以访问共享存储空间、其它目录下的文件了。

Android 6.0 之后的访问方式动态申请权限

Android 6.0 后需要动态申请权限,除了在AndroidManifest.xml 里声明存储权限外,还需要在代码里动态申请。

//申请权限privatevoidrequestPermission( Activity activity, String requestPermissionList[]) { ActivityCompat.requestPermissions(activity, requestPermissionList, 100); }

//用户作出选择后,返回申请的结果@ OverridepublicvoidonRequestPermissionsResult( intrequestCode, @NonNull String[] permissions, @NonNull int[] grantResults ) { if(requestCode == 100) { for( inti = 0; i < permissions.length; i++) { if(permissions[i]. equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { if(grantResults[i] == PackageManager.PERMISSION_GRANTED) { Toast.makeText(MainActivity. this, "存储权限申请成功", Toast.LENGTH_SHORT).show; } else{ Toast.makeText(MainActivity. this, "存储权限申请失败", Toast.LENGTH_SHORT).show; }}}}}

//测试申请存储权限privatevoidtestPermission( Activity activity) { String[] checkList = newString[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}; List needRequestList = checkPermission(activity, checkList);if(needRequestList.isEmpty) { Toast.makeText(MainActivity. this, "无需申请权限", Toast.LENGTH_SHORT).show; } else{ requestPermission(activity, needRequestList.toArray( newString[needRequestList.size])); }}

申请权限后,提示用户作出选择:

访问文件

权限申请成功后,即可对自带外部存储之共享存储空间和其它目录进行访问。

分别以共享存储空间和其它目录为例,阐述访问方式:

访问共享存储空间

共享存储空间分为两类文件:媒体文件和文档/其它文件。

访问媒体文件

目的是拿到媒体文件的路径,有两种方式获取路径:

1、直接构造路径

以图片为例,假设图片存储在/sdcard/Pictures/目录下。

如上,myPic.png的路径:/storage/emulated/0/Pictures/myPic.png,拿到路径后就可以解析并获取Bitmap。

2、通过MediaStore获取路径

沿用上篇的demo:

同样的,也是拿到图片路径后获取Bitmap。

还有一种不直接通过路径访问的方法:

3、通过MediaStore获取Uri

与直接拿到路径不同的是,此处拿到的是Uri。图片的信息封装在Uri里,通过Uri构造出InputStream,再进行图片解码拿到Bitmap

访问文档和其它文件

1、直接构造路径

与媒体文件一样,可以直接构造路径访问。

2、通过SAF访问

Storage Access Framework 简称SAF:存储访问框架。相当于系统内置了文件选择器,通过它可以拿到想要访问的文件信息。

同样的以获取图片为例:

@OverrideprotectedvoidonActivityResult( intrequestCode, intresultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data);

if(requestCode == 100) { //选中返回的图片封装在uri里Uri uri = data.getData;openUri(uri);}}

privatevoidopenUri(Uri uri){ try{ //从uri构造输入流InputStream fis = getContentResolver.openInputStream(uri);Bitmap bitmap = BitmapFactory.decodeStream(fis);} catch(Exception e) {

}}

可以看出,通过SAF并不能直接拿到图片的路径,图片的信息封装在Uri里,通过Uri构造出InputStream,再进行图片解码拿到Bitmap

访问其它目录

有两种方式:

1、直接构造路径

在/sdcard/目录下直接创建目录:

可以看出,/sdcard/myDir/目录创建成功。

2、通过SAF访问

与共享存储空间SAF访问方式一致。

Android 10.0 之前访问方式总结

由上面分析的共享存储空间/其它目录访问方式可知,访问目录/文件可通过如下两个方法:

1、通过路径访问。路径可以直接构造也可以通过MediaStore获取。

2、通过Uri访问。Uri可以通过MediaStore或者SAF获取。

1、通过路径访问。路径可以直接构造也可以通过MediaStore获取。

2、通过Uri访问。Uri可以通过MediaStore或者SAF获取。

Android 6.0 以下访问共享存储空间/其它目录步骤:

1、AndroidManifest.xml里声明存储权限

2、通过路径或者Uri访问文件

1、AndroidManifest.xml里声明存储权限

2、通过路径或者Uri访问文件

Android 6.0(含)~Android 10.0(不含)访问共享存储空间/其它目录步骤:

1、AndroidManifest.xml里声明存储权限

2、动态申请存储权限

3、通过路径或者Uri访问文件

1、AndroidManifest.xml里声明存储权限

2、动态申请存储权限

3、通过路径或者Uri访问文件

3、Android 10.0 访问方式变更

为什么要变更

你可能已经发现了上面访问方式的弊端,比如我们能够直接在/sdcard/目录下创建目录/文件。事实上,很多App就是这么干的,看图说话:

可以看出/sdcard/目录下,如淘宝、qq、qq浏览器、微博、支付宝等都自己建了目录。

这么看来,导致目录结构很乱,而且App卸载后,对应的目录并没有删除,于是就是遗留了很多"垃圾"文件,久而久之不处理,用户的存储空间越来越小。

总结弊端如下:

1、在设置里"Clear storage"或者"Clear cache"并不能删除该目录下的文件

2、卸载App也不能删除该目录下的文件

3、App可以随意修改其它目录下的文件,如修改别的App创建的文件等,不安全

1、在设置里"Clear storage"或者"Clear cache"并不能删除该目录下的文件

2、卸载App也不能删除该目录下的文件

3、App可以随意修改其它目录下的文件,如修改别的App创建的文件等,不安全

你也许会问,为什么要在/sdcard/目录下新建自己的目录呢?

大体有以下两个原因:

1、此处新建的目录不会被设置里的App存储用量统计,让用户"看起来"自己的App占用的存储空间很小

2、方便操作文件

1、此处新建的目录不会被设置里的App存储用量统计,让用户"看起来"自己的App占用的存储空间很小

2、方便操作文件

面对众多App不讲"码德"随意新建目录/文件的现象,Google在Android 10.0上重拳出击了。

引入Scoped Storage

翻译成中文有好几个版本:作用域存储、分区存储、沙盒存储。

具体中文翻译不重要,下面以分区存储指代。

分区存储原理:

1、App访问自身内部存储空间、访问外部存储空间-App私有目录不需要任何权限(这个与Android 10.0之前一致)

2、外部存储空间-共享存储空间、外部存储空间-其它目录 App无法通过路径直接访问,不能新建、删除、修改目录/文件等

3、外部存储空间-共享存储空间、外部存储空间-其它目录 需要通过Uri访问

1、App访问自身内部存储空间、访问外部存储空间-App私有目录不需要任何权限(这个与Android 10.0之前一致)

2、外部存储空间-共享存储空间、外部存储空间-其它目录 App无法通过路径直接访问,不能新建、删除、修改目录/文件等

3、外部存储空间-共享存储空间、外部存储空间-其它目录 需要通过Uri访问

分区存储的变更在于第二点、第三点。

为什么Uri能够访问

先来看为什么通过路径无法直接访问。

我们知道访问文件最终是通过构造InputStream/OutputStream来实现的,以InputStream为例,看看其构造方法:

可以看出,要想FileInputStream 能读入文件,核心是需要构造FileDeor,而对于Android 10.0,直接通过路径构造FileDeor 会抛出异常。

那么我们自然会想到,有没有通过构造好的FileDeor 来生成FileInputStream对象,进而使用read(xx)方法读取数据。

还真有,请看:通过Uri构造InputStream。

进入看其源码:

AssetFileDeor 持有ParcelFileDeor 引用,而ParcelFileDeor 持有FileDeor 引用。

同理也适用于FileOutputStream。因此,通过Uri能够访问文件。

4、如何不适配Android 10.0

从以上分析可知,适配Android 10.0 有点麻烦,问题来了有没有简单的方法绕过检测。

第一种方法

1、Android 10.0 及其以后才会有分区存储功能,只要Android 设备不升级系统到Android 10.0以后,就不会有问题。

2、可能觉得这是句废话,其实不然,有些定制的设备系统一般都不会升级的。

1、Android 10.0 及其以后才会有分区存储功能,只要Android 设备不升级系统到Android 10.0以后,就不会有问题。

2、可能觉得这是句废话,其实不然,有些定制的设备系统一般都不会升级的。

如果不能使用第一种方法,还可以采用第二种方法。

第二种方法

1、Android 一般升级功能的时候都会配合targetSdkVersion使用。只要targetSdkVersion= Build.VERSION_CODES.Q) { //RELATIVE_PATH 字段表示相对路径-------->(1)contentValues.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);} else{ StringdstPath = Environment.getExternalStorageDirectory + File.separator + Environment.DIRECTORY_PICTURES + File.separator + fileName;//DATA字段在Android 10.0 之后已经废弃contentValues.put(MediaStore.Images.ImageColumns.DATA, dstPath);}

//插入相册------->(2)Uri uri = getContentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);

//写入文件------->(3)write2File(uri, inputStream);}

重点说明三个点:

(1)

Android 10.0之前,MediaStore.Images.ImageColumns.DATA 字段记录的是图片的绝对路径,而Android 10.0(含)之后,DATA 被废弃,取而代之的是使用MediaStore.Images.ImageColumns.RELATIVE_PATH,表示相对路径。比如指定RELATIVE_PATH为Environment.DIRECTORY_PICTURES,表示之后的图片将会放到Environment.DIRECTORY_PICTURES目录下。

(2)

调用ContentResolver里的方法插入相册。

MediaStore.Images.Media.EXTERNAL_CONTENT_URI 指的是插入图片表。

ContentValues 以Map的形式记录了待写入的字段值。

插入后返回Uri。

(3)

以上两步仅仅只是往数据库里增加一条记录,该记录指向的新文件是空的,需要将图片写入到新文件。

而新文件位于/sdcard/Pictures/目录下,该目录是不能直接通过路径访问的,因此需要通过第二步返回的Uri进行访问。

try{ //从Uri构造输出流OutputStream outputStream = getContentResolver.openOutputStream(uri);

byte[] in= newbyte[ 1024]; intlen = 0;

do{ //从输入流里读取数据len = inputStream.read( in); if(len != -1) { outputStream.write( in, 0, len); outputStream.flush;}} while(len != -1);

inputStream.close;outputStream.close;

} catch(Exception e) { Log.d( "test", e.getLocalizedMessage); }}

可以看出,目标文件关联的Uri有了,还需要原始的输入文件。

测试上述的插入方法:

String picName = "mypic.jpg"; try{ File externalFilesDir = getExternalFilesDir( null); File file = newFile(externalFilesDir, picName); FileInputStream fis = newFileInputStream(file); insert2Album(fis, picName);} catch(Exception e) { Log.d( "test", e.getLocalizedMessage); }}

其中,原始文件(图片)存放于自带外部存储-App私有目录,如下:

需要注意的是:

1、读取原始文件需要权限,上述例子里的原始文件存放在自带外部存储-App私有目录,因此本App可以使用路径直接读取

2、对于其他目录则依然需要构造Uri读取,如通过SAF获取Uri

1、读取原始文件需要权限,上述例子里的原始文件存放在自带外部存储-App私有目录,因此本App可以使用路径直接读取

2、对于其他目录则依然需要构造Uri读取,如通过SAF获取Uri

同样的,想要从系统相册中获取图片,也需要通过Uri访问。

if(cursor != null) { while(cursor.moveToNext) { //获取唯一的idlongid = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)); //通过id构造UriUri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);//解析uridecodeUriForBitmap(uri);}}}

privatevoiddecodeUriForBitmap( Uri uri) { if(uri == null) return;

try{ //构造输入流InputStream inputStream = getContentResolver.openInputStream(uri);//解析BitmapBitmap bitmap = BitmapFactory.decodeStream(inputStream);if(bitmap != null) Log.d( "test", "bitmap width-width:"+ bitmap.getWidth + "-"+ bitmap.getHeight); } catch(Exception e) { Log.d( "test", e.getLocalizedMessage); }}

与插入相册过程类似,同样需要拿到Uri,再构造输入流,从输入流读取文件(图片内容)。

以上,通过Uri 获取图片和插入相册分析完毕,共享存储空间的其他文件类型如视频、音频、下载文件也是同样的流程。

需要说明的是上述的ContentResolver .insert(xx)/ContentResolver.query(xx) 的参数取值还可以更丰富,但不是本篇重点,因此忽略了,实际使用过程中具体情况具体分析。

8、Android 11.0 权限申请

通过Uri访问文件似乎已经满足了Android 10.0适配要求,但是仔细想想还是有不足之处:

1、共享存储空间只能通过MediaStore访问,以前流行的访问方式是直接通过路径访问。比如自己做的相册管理器,先遍历相册拿到图片/视频的路径,然后再解析成Bitmap展示,现在需要先拿到Uri,再解析成Bitmap,多少有些不方便。此外,也许你依赖的第三方库是直接通过路径访问文件的,而三方库又没有及时更新适配分区存储,可能就会导致用不了相应的功能。

2、SAF虽然能够访问其它目录的文件,但是每次都需要跳转到新的页面去选择,当想要批量展示文件的时候,比如自己做的文件管理器,就需要列出当前目录下有哪些目录/文件,这个时候需要有权限遍历/sdcard/目录。显然,SAF并不能胜任此工作。

1、共享存储空间只能通过MediaStore访问,以前流行的访问方式是直接通过路径访问。比如自己做的相册管理器,先遍历相册拿到图片/视频的路径,然后再解析成Bitmap展示,现在需要先拿到Uri,再解析成Bitmap,多少有些不方便。此外,也许你依赖的第三方库是直接通过路径访问文件的,而三方库又没有及时更新适配分区存储,可能就会导致用不了相应的功能。

2、SAF虽然能够访问其它目录的文件,但是每次都需要跳转到新的页面去选择,当想要批量展示文件的时候,比如自己做的文件管理器,就需要列出当前目录下有哪些目录/文件,这个时候需要有权限遍历/sdcard/目录。显然,SAF并不能胜任此工作。

Android 11.0考虑到上面的问题,因此做了新的优化。

共享存储空间-媒体文件访问变更

媒体文件可以通过路径直接访问:

try{ //取出路径String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));Bitmap bitmap = BitmapFactory.decodeFile(path);} catch(Exception e) { Log.d( "test", e.getLocalizedMessage); }break; }}

可以看出,之前在Android 10.0上被禁用的访问方式,在Android 11.0上又被允许了,这就解决了上面的第一个问题。

需要注意的是:此种方式只允许读文件,写文件依然不行

Google 官方指导意见是:

虽然可以通过路径直接访问媒体文件,但是这些操作最终是被重定向到MediaStore API的,重定向过程可能会损耗一些性能,并且直接通过路径访问不一定比MediaStore API 访问快。

总之建议非必要的话不要直接使用路径访问。

虽然可以通过路径直接访问媒体文件,但是这些操作最终是被重定向到MediaStore API的,重定向过程可能会损耗一些性能,并且直接通过路径访问不一定比MediaStore API 访问快。

总之建议非必要的话不要直接使用路径访问。

假若App开启了分区存储功能,当App运行在Android 10.0的设备上时,是没法遍历/sdcard/目录的。而在Android 11.0上运行时是可以遍历的,需要进行如下几个步骤。

1、声明管理权限

在AndroidManifest.xml添加权限声明

2、动态申请所有文件访问权限

@OverrideprotectedvoidonActivityResult( intrequestCode, intresultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); //申请权限结果if(requestCode == 101) { if(Environment.isExternalStorageManager) { Toast.makeText(MainActivity. this, "访问所有文件权限申请成功", Toast.LENGTH_SHORT).show;

//遍历目录showAllFiles;}}}

此处申请权限不是以对话框的形式提示用户,而是跳转到新的页面,说明该权限的管理更严格。

3、遍历目录、读写文件

拥有权限后,就可以进行相应的操作了。

文件管理器效果图类似如下:

当然读写文件也不在话下了,比如往/sdcard/目录下写入文件:

ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 这个权限的名字看起来很唬人,感觉就像是能够操作所有文件的样子,这不就是打破了分区存储的规则了吗?其实不然:

即使拥有了该权限,依然不能访问内部存储和外部存储-App私有目录

即使拥有了该权限,依然不能访问内部存储和外部存储-App私有目录

需要说明的是:

1、Environment.isExternalStorageManager、Build.VERSION_CODES.R 等需要编译版本>=30才能编译通过。

2、Google 提示当使用MANAGE_EXTERNAL_STORAGE 申请权限时,并且targetSdkVersion>=30,此种情况下App被禁止上架Google Play的,限制时间最早到2021年。因此,在此时间之前若是申请了MANAGE_EXTERNAL_STORAGE权限,最好不要升级targetSdkVersion到30以上。

1、Environment.isExternalStorageManager、Build.VERSION_CODES.R 等需要编译版本>=30才能编译通过。

2、Google 提示当使用MANAGE_EXTERNAL_STORAGE 申请权限时,并且targetSdkVersion>=30,此种情况下App被禁止上架Google Play的,限制时间最早到2021年。因此,在此时间之前若是申请了MANAGE_EXTERNAL_STORAGE权限,最好不要升级targetSdkVersion到30以上。

9、Android 10/11 存储适配建议

好了,通过分析Android 10/11存储适配方式,了解到了不同的系统需要如何进行适配,此时就需要一个统一的适配方案了。

适配核心

分区存储是核心,App自身产生的文件应该存放在自己的目录下:

/sdcard/Android/data/packagename/ 和/data/data/packagename/

/sdcard/Android/data/packagename/ 和/data/data/packagename/

这两个目录本App无需申请访问权限即可申请,其它App无法访问本App的目录。

适配共享存储

共享存储空间里的文件需要通过Uri构造输入输出流访问,Uri获取方式有两种:MediaStore和SAF。

适配其它目录

在Android 11上需要申请访问所有文件的权限。

具体做法第一步

在AndroidManifest.xml里添加如下字段:

权限声明:

在标签下添加如下字段:

第二步

如果需要访问共享存储空间,则判断运行设备版本是否大于等于Android6.0,若是则需要申请WRITE_EXTERNAL_STORAGE 权限。拿到权限后,通过Uri访问共享存储空间里的文件。

如果需要访问其它目录,则通过SAF访问

第三步

如果想要做文件管理器、病毒扫描管理器等功能。则判断运行设备版本是否大于等于Android 6.0,若是先需要申请普通的存储权。若运行设备版本为Android 10.0,则可以直接通过路径访问/sdcard/目录下文件(因为禁用了分区存储);若运行设备版本为Android 11.0,则需要申请MANAGE_EXTERNAL_STORAGE 权限。

以上是Android 存储权限适配的全部内容。

本篇基于Android 10.0 11.0 。Android 10.0真机、Android 11.0模拟器

测试代码(https://github.com/fishforest/AndroidDemo/tree/main/app/src/main/java/com/example/androiddemo/storagepermission)

1、滴滴、满帮、Boss直聘都被调查,为啥知乎美国上市没被查?

2、字节跳动重大宣布:取消!员工炸了:直接降薪1

3、再见了,Teamviewer!

5、程序员被公司辞退12天,前领导要求回公司讲清楚代码,结果懵了 返回搜狐,查看更多



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3